淺談 throw 與 throw ex 的差異
TLDR
- 在
catch區塊中,應優先使用throw;而非throw ex;。 throw;能保留原始的 Stack Trace,確保除錯時能定位到錯誤發生的原始行數。throw ex;會將當前位置視為錯誤起點,導致原始堆疊資訊遺失,應避免使用。- 若需封裝錯誤,應建立新的 Exception 並將原始 Exception 作為
innerException傳入。 - 若
catch區塊內僅執行throw;而無其他邏輯,應直接移除try...catch區塊。 - .NET 5 以上版本已引入
CA2200規則,會自動偵測並警告throw ex的使用。
throw 和 throw ex 的使用方式
使用 throw 來重新拋出 Exception
什麼情況下會遇到這個問題:當需要在 catch 區塊中攔截錯誤,執行部分邏輯後,仍希望將錯誤繼續向上拋出,且不希望破壞原始錯誤堆疊時。
使用 throw 可以重新拋出捕捉到的 Exception,並保留原始的 Exception 堆疊資訊。這對於除錯非常重要,因為它能夠提供正確的 Exception 發生路徑。
try {
try {
int result = Divide(1, 0);
} catch {
throw;
}
} catch (Exception e) {
Console.WriteLine(e.ToString());
}
static int Divide(int numerator, int denominator) {
return numerator / denominator;
}產生的錯誤訊息如下,可以明確地知道是第 3 行發生錯誤:
System.DivideByZeroException: Attempted to divide by zero.
at Program.<<Main>$>g__Divide|0_0(Int32 numerator, Int32 denominator) in D:\Programming\Projects\TestThrow\TestThrow\Program.cs:line 12
at Program.<Main>$(String[] args) in D:\Programming\Projects\TestThrow\TestThrow\Program.cs:line 3使用 throw ex 會導致的問題
什麼情況下會遇到這個問題:開發者誤以為 throw ex 是重新拋出錯誤的標準寫法,卻忽略了它會重置堆疊資訊。
使用 throw ex 則會建立一個新的 Exception 並拋出,這會重置 Exception 的堆疊資訊,使得追蹤 Exception 變得更加困難。
try {
try {
int result = Divide(1, 0);
} catch (Exception ex) {
throw ex;
}
} catch (Exception e) {
Console.WriteLine(e.ToString());
}
static int Divide(int numerator, int denominator) {
return numerator / denominator;
}顯示的錯誤訊息如下,可以發現堆疊資訊裡,錯誤發生變成是第 5 行 throw ex 的位置,導致無法找到真正出錯的行數:
System.DivideByZeroException: Attempted to divide by zero.
at Program.<Main>$(String[] args) in D:\Programming\Projects\TestThrow\TestThrow\Program.cs:line 5TIP
測試顯示使用 throw ex 重新拋出的 Exception,其 InnerException 為 null,這證實了原本的堆疊資訊確實會遺失。
從 .NET 5 開始,官方增加了 CA2200 的檢核規則,當偵測到 throw ex 的程式碼,會出現警告 CA2200: 重新擲回攔截到的例外狀況變更堆疊資訊。詳細說明請參考 重大變更:CA2200:重新擲回以儲存堆疊詳細資料 和 CA2200: 必須重新擲回以儲存堆疊詳細資料。
正確的 Exception 處理方式
什麼情況下會遇到這個問題:當需要對原始 Exception 進行封裝,並添加額外的業務邏輯上下文資訊時。
我們應該重新拋出一個新的 Exception,並將原始 Exception 作為內部 Exception 傳遞。這樣做能夠保留原始 Exception 的資訊,同時增加新的上下文資訊。
public class CustomException : Exception {
public CustomException(string message, Exception innerException)
: base(message, innerException) {
}
}
try {
// 可能發生 Exception 的程式碼...
} catch (Exception ex) {
// ex 要作為 InnerException 傳入,才能保留資訊
throw new CustomException("額外的資訊", ex);
}避免不必要的 try...catch
什麼情況下會遇到這個問題:在程式碼重構過程中,遺留了僅包含 throw; 的空 catch 區塊。
如果在 catch 區塊內沒有任何處理僅僅是 throw 的話,實際上可以省略 try...catch,因為這樣的 catch 區塊並沒有任何意義。
try {
// 可能發生 Exception 的程式碼...
} catch {
// 沒做任何處理,只有 throw
throw;
}異動歷程
- 初版文件建立。